;重构：感觉滚动屏幕与获取目标字母的动作部分重合，是否要整合到一起？
;重构：部分地方涉及除法溢出的风险，如random、show_num模块
;头部
SECTION head align=16 vstart=0 
    ;1 用户程序长度
    program_length dd program_end       ;[0x00]
    ;2 程序入口
    code_entry:
        dw start
        dd section.code.start   ;疑惑；为什么要用32位存放段的汇编地址？16位难道放不下吗？后面的段重定位表也是
                                ;注意主引导程序中phy_base的大小；16位的地址范围只有64k，放汇编地址当然会放不下。
    ;3 重定位表
    realloc_tbl_len dw (head_end - code_segment) / 4

    code_segment dd section.code.start  ;[0x0c]
    data_segment dd section.data.start  ;[0x10]
    stack_segment dd section.data.start ;[0x14]

    head_end:                   ;疑惑：这个冒号可以不加吗？

;代码段
SECTION code align=16 vstart=0
;程序入口点
start:
    ;备注：主引导扇区程序会将ds指向用户程序头部
    mov ax,[data_segment]
    mov ds,ax

    call install_int9   ;安装新的键盘中断例程
    call init_screen 
    
    mov ah,0 
    int 0x16            ;按任意键后开始游戏

    mov bx,1            ;迭代待显示的字符
    mov cx,80
    s:
        call delay2

        mov ax,[bottom_letter]
        mov di,0x0720           ;黑底白字, 空格
        call surround_char
        
        cli                 ;避免刚移动一行，还没有记录新的字母位置，中断就来了
        call down_screen
        call get_bottom
        sti

        call show_bottom

        mov ax,[bottom_letter]
        mov di,0x072b           ;黑底蓝绿字, -号
        call surround_char

        mov ax,[score]
        mov di, 24 * 160 + (score_label_end - score_label) * 2
        call show_num

        mov ax,cx 
        mov dl,2
        div dl 
        cmp ah,0
        je not_appear   ;cx是奇数时才显示字符
        
            call random_letter      ;(al)=随机字母
            call appear_char_top
            inc bx
        not_appear:

        loop s 

    hello: jmp near hello

;1 显示模块 - 辅助子程序
put_char:
    ;说明：在屏幕指定位置显示一个字符，传入的行列号都是从0开始
    ;参数：(dh)=行号, (dl)=列号, (al)=待显示字符
    push es 
    push bx
    push ax 

    mov bx,0        ;存放显示的目标位置
    mov al,0xa0
    mul dh 
    add bx,ax       ;bx += 0xa0 * dh(行号)
    mov al,2 
    mul dl 
    add bx,ax       ;bx += 2 * dl(列号)
    
    mov ax,0xb800
    mov es,ax       ;es指向显存

    pop ax
    mov es:[bx], al

    pop bx
    pop es 
    ret

put_char_with_style:
    ;说明：在屏幕指定位置显示一个字符，传入的行列号都是从0开始
    ;参数：(dh)=行号, (dl)=列号, (al)=待显示字符, (ah)=字符属性
    ;重构：与put_char代码高度相似
    push es 
    push bx
    push ax 

    mov bx,0        ;存放显示的目标位置
    mov al,0xa0
    mul dh 
    add bx,ax       ;bx += 0xa0 * dh(行号)
    mov al,2 
    mul dl 
    add bx,ax       ;bx += 2 * dl(列号)
    
    mov ax,0xb800
    mov es,ax       ;es指向显存

    pop ax
    mov es:[bx], ax

    pop bx
    pop es 
    ret

appear_char_top:
    ;说明：在屏幕第一行随机位置，显示一个字符
    ;参数：(al)=待显示的字符
    push dx
    
    mov dx,78
    call random_range       ;返回(dx)=0~77的随机数

    inc dx                  ;1~78, 避免在边框上

    mov dh,0
    call put_char           ;列号dl即在子程序random_range的返回值中

    pop dx 
    ret

surround_char:
    ;说明：以指定字符为中心，周围显示一圈字符将它包裹起来
    ;参数：(ax)=字符位置, (di)=环绕字符的ascii码及其属性
    push bx 
    push cx 
    push dx
    
    ;1 从全屏偏移值，解析出行列号(dh,dl)
    mov dx,0
    mov bx,160
    div bx          ;(dl)=列号*2, (al)=行号

    mov dh,al

    mov ax,0
    mov al,dl 
    mov bl,2
    div bl
    mov ah,0        ;不过余数应该本来就会是0

    mov dl,al

    ;2 样式表surround_style显示环绕效果
    mov ax, style_end - surround_style
    mov bx, 2 
    div bl 
    mov ah,0        ;有点多余，但比较保险
    mov cx,ax 

    mov bx,0
    put_style:
        push dx 
        mov ax, surround_style[bx]

        add dx, ax          ;叠加基础行列号(dx)，与相对行列号(ax)
        mov ax, di 
        call put_char_with_style       ;问题：可是这样就利用不到di中的属性了

        add bx, 2
        pop dx 
        loop put_style

    pop dx 
    pop cx 
    pop bx 
    ret
    
down_screen:
    ;说明：屏幕下滚一行，实现字母下落效果
    ;备注：最底部一行用于显示一些状态信息，不操作
    ;重构：感觉和子程序mov_row代码形式相同，只是一些立即数不同
    push es
    push ax 
    push bx 
    push cx 

    mov bx,0xb800
    mov es,bx 

    mov bx, 23 * 160 + 158      ;共25行，第24行右端
    mov cx, 23 * 80             ;要移动的字符数 
    s_down:
        mov ax, es:[bx - 160]
        mov es:[bx], ax 
        sub bx,2
        loop s_down

    mov bx,0
    mov cx,80
    s_clear_top:
        mov word es:[bx], 0x0720     ;清空第一行
        add bx,2
        loop s_clear_top

    pop cx 
    pop bx 
    pop ax 
    pop es 
    ret

delay:
    ;说明：利用空循环让程序暂停一会儿
    push cx 
    mov cx,0xffff
    s_delay:
        loop s_delay
    pop cx 
    ret

delay2:
    ;说明：循环调用delay，让程序暂停较长时间
    push cx 
    mov cx,0xcff 
    s_delay2:
        call delay
        loop s_delay2
    pop cx 
    ret

get_bottom:
    ;说明：在屏幕上搜索最靠下的字母坐标，并保存到数据段bottom_letter中
    push es 
    push ax
    push bx 
    push cx

    mov bx,0xb800 
    mov es,bx 

    mov bx, 23 * 160 + 158
    mov cx, 24 * 80
    s_bottom:
        mov ax, es:[bx]
        cmp al,'A'
        jb not_letter_
        cmp al,'Z'
        ja not_letter_
        cmp ah,0x07
        jne not_letter_

        ;if('A'<= al && al <= 'Z' && al是黑底白字) { head_letter = bl; }
        mov [bottom_letter], bx    
        mov cx,1        ;退出循环
        not_letter_:

        sub bx,2
        loop s_bottom    

    pop cx
    pop bx 
    pop ax 
    pop es 
    ret

show_bottom:
    ;说明：在屏幕右下角区域显示头字母
    ;返回：(al)=头字符
    push bx
    push es

    mov bx,0xb800 
    mov es,bx 

    ; mov bx,0
    mov bx,[bottom_letter]
    mov al, es:[bx]

    mov dh,24
    mov dl,77
    call put_char   ;显示当前头字符的位置, 前bx可能不止一位数呀
    pop es 
    pop bx
    ret 

init_screen:
    ;清屏，置为黑底白字属性的空格
    push es 
    push ax 
    push bx 
    push cx
    push si

    mov bx,0xb800 
    mov es,bx 

    mov bx,0
    mov cx,2000
    s_clear:
        mov word es:[bx], 0x0720 
        add bx,2
        loop s_clear

    mov si, 0
    mov bx, 24 * 160
    mov cx, score_label_end - score_label
    s_score:
        mov al, score_label[si]
        mov es:[bx], al
        mov byte es:[bx + 1], 011b      ;蓝绿色
        inc si 
        add bx,2
        loop s_score

    pop si
    pop cx
    pop bx 
    pop ax 
    pop es 
    ret

;2 键盘模块 - 辅助子程序
install_int9:
    ;说明：安装一个新的int 9中断例程，在按下键盘时进行自定义的操作
    ;重构：可以调整一下各个步骤的顺序，有机会减少冗余
    push si 
    push di
    push ax
    push cx
    push ds

    ;将旧的int 9中断例程的中断号修改为int 128
    mov ax,0
    mov ds,ax           ;ds段指向中断向量表

    mov ax, [9*4]
    mov [128*4], ax 
    mov ax, [9*4+2]
    mov [128*4+2], ax   ;修改中断向量表

    ;将新中断例程的入口点写入中断向量表
    mov ax,0
    mov ds,ax           ;ds指向中断向量表
    
    cli
    mov word [9*4], int9_new
    mov word [9*4+2], cs
    sti  

    pop ds 
    pop cx 
    pop ax 
    pop di 
    pop si 
    ret
 
int9_new:
    ;说明：新的int 9中断例程，在屏幕固定位置显示按下的字符
    ;注意：需保证中断前ds正指向用户程序的数据段
    push dx
    push ax
    push bx 
    push es

    int 128         ;在安装本中断前，已将旧int 9的中断号修改为128。这句好像不要也没关系

    in al,0x60          ;读扫描码
    cmp al,0x53
    ja not_general      ;扫描码值大于常用通码 --> 跳过不显示

    xor bx,bx 
    mov bl,al
    mov ah, key_table[bx]       ;(ah)=扫描码对应的字母ascii码

    mov al,ah
    mov dh,24
    mov dl,79
    call put_char               ;在屏幕右下角显示按下的字母

    call show_bottom            ;(al)=头字符，返回值
    cmp ah,al 
    jne wrong_letter

        mov ax,0xb800
        mov es,ax
        
        mov ah,0x02 
        mov bx,[bottom_letter]
        mov es:[bx + 1], ah         ;将头字符的属性修改为绿色

        mov ax,[bottom_letter]
        mov di,0x0720               ;黑底白字, 空格 --> 解除环绕
        call surround_char

        mov ax,[score]
        inc ax 
        mov [score],ax              ;增加分数

        call get_bottom             ;立即更新头字符
    wrong_letter:

    not_general:
    pop es
    pop bx
    pop ax 
    pop dx 
    iret

;3 其他模块 - 辅助子程序
random:
    ;说明：生成一个随机数，采用线性同余法，范围0~252
    ;返回：(dx)=随机数
    push ax

    mov al,[random_seed]
    mov dl,params[0]
    mul dl              ;(ax)=al * dl

    mov dl,params[1]
    add al,dl 

    mov dl,params[2]
    div dl              ;(ah)=ax % dl, 即最终随机数

    mov [random_seed],ah    ;迭代随机数种子 
    mov dx,0                
    mov dl,ah               ;返回随机数(dx)=ah

    pop ax
    ret

random_range:
    ;说明：指定范围，生成一个随机数
    ;参数：(dl-1)=生成随机数的最大值
    ;返回：(dx)=随机数
    push ax 

    push dx
    call random
        
    mov ax,dx       ;(ax)=原随机数
    pop dx          ;(dl)=范围
    div dl          ;(ah)=新随机数

    mov dx,0 
    mov dl,ah       ;(dx)=新随机数

    pop ax 
    ret

random_letter:
    ;说明：生成一个随机的大写字母
    ;返回：(ax)=随机字母
    push dx 

    mov dx,26
    call random_range
    add dx,'A'
    mov ax,dx

    pop dx 
    ret

show_num:
    ;说明：显示内存中的一个数字，以10进制的形式
    ;参数：(ax)=待显示的数字, di=显示位置
    push ax
    push bx 
    push cx 
    push dx 
    push es 

    ;1 求得每位的数字，放入栈中
    mov bx,10
    mov dx,0
    mov cx,0        ;记录求得的位数
    div_num:
        div bx      ;(dx)=余数，即求得当前(dx,ax)的最低一位
        push dx 
        inc cx
        cmp ax, 0
        je no_div

        mov dx,0
        jmp div_num
    no_div:

    ;2 将数字逐位显示到屏幕上
    mov bx,0xb800
    mov es,bx 

    put_one:
        pop dx
        add dx, '0'
        mov es:[di], dl

        add di, 2
        loop put_one

    pop es 
    pop dx 
    pop cx 
    pop bx
    pop ax 
    ret
        
;数据段
SECTION data align=16 vstart=0
    text db 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    score_label db 'Score:' 
        score_label_end:
    score dw 0              ;按中字符的个数

    key_table:
        ;说明：以扫描码为相对地址，即可获取26个字母对应的ascii码。
        ;(表地址范围)=0x00~0x53
        db 16 dup (0)
        db 'QWERTYUIOP'
        db 4 dup (0)
        db 'ASDFGHJKL'
        db 5 dup (0)
        db 'ZXCVBNM'
        db 33 dup (0)
    surround_style:
        db -1,-2,   0,-1,   1,-1
        db -1,-1,   1,0,
        db -1,0,    0,1,    1,1
        style_end:
    bottom_letter dw 0xffff      ;屏幕最靠下字母的实时位置, 整个字是一个偏移值
    random_seed db 1        ;以此生成下一个随机数，每次生成后会更新
    params db 217,11,253    ;线性同余法的参数

;结束
SECTION trail align=16
    ;说明：不指定vstart，program_end的值才是整个用户程序尾的汇编地址，否则为vstart指定的值。
    program_end:
